/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import Plugin, { type PluginConstructor } from '@vaadin/hilla-generator-core/Plugin.js';
import type LoggerFactory from '@vaadin/hilla-generator-utils/LoggerFactory.js';
import GeneratorIOException from './GeneratorIOException.js';

type PluginConstructorModule = Readonly<{
  default: PluginConstructor;
}>;

export const GENERATED_LIST_FILENAME = 'generated-file-list.txt';

export default class GeneratorIO {
  readonly #logger: LoggerFactory;
  readonly #outputDir: URL;

  constructor(outputDir: URL, logger: LoggerFactory) {
    this.#outputDir = outputDir;
    this.#logger = logger;

    logger.global.debug(`Output directory: ${this.#outputDir}`);
  }

  /**
   * Gets the list of files generated the last time. The info is found in {@link GENERATED_LIST_FILENAME}.
   * @returns a list of files that have been generated by us
   */
  async getExistingGeneratedFiles(): Promise<readonly string[]> {
    const files = new Set<string>();
    try {
      const contents = await this.read(GENERATED_LIST_FILENAME);
      contents
        .split('\n')
        .filter((n) => n.length)
        .forEach((fileName) => files.add(fileName));
    } catch (e) {
      // non-existing file is OK, all other errors must be rethrown
      if (!(e instanceof Error && 'code' in e && e.code === 'ENOENT')) {
        throw e;
      }
    }
    return Array.from(files);
  }

  /**
   * Cleans the output directory by keeping the generated files and deleting the rest of the given files.
   *
   * @returns a list with names of deleted files
   */
  async cleanOutputDir(
    generatedFiles: readonly string[],
    filesToDelete: readonly string[],
  ): Promise<readonly string[]> {
    this.#logger.global.debug(`Cleaning ${this.#outputDir}`);
    await mkdir(this.#outputDir, { recursive: true });

    const filtered = filesToDelete.filter((item) => !generatedFiles.includes(item));

    return Array.from(
      new Set(
        await Promise.all(
          filtered.map(async (filename) => {
            const url = new URL(filename, this.#outputDir);
            try {
              await rm(url);
              this.#logger.global.debug(`Deleted file ${url}.`);
              return filename;
            } catch (e: unknown) {
              this.#logger.global.debug(`Cannot delete file ${url}: ${e instanceof Error ? e.message : String(e)}`);
              return undefined;
            }
          }),
        ).then((files) => files.filter((filename) => filename != null)),
      ),
    );
  }

  async writeGeneratedFiles(files: readonly File[]): Promise<readonly string[]> {
    await this.write(
      new File(
        files.map((file) => `${file.name}\n`),
        GENERATED_LIST_FILENAME,
      ),
    );

    return Promise.all(
      files.map(async (file) => {
        const newFileContent = await file.text();
        let oldFileContent;
        try {
          oldFileContent = await this.read(file.name);
        } catch (_e) {}

        if (newFileContent !== oldFileContent) {
          await this.write(file);
        } else {
          this.#logger.global.debug(`File ${new URL(file.name, this.#outputDir)} stayed the same`);
        }
        return file.name;
      }),
    );
  }

  async loadPlugin(modulePath: string): Promise<PluginConstructor> {
    this.#logger.global.debug(`Loading plugin: ${modulePath}`);
    const module: PluginConstructorModule = await import(modulePath);
    const ctr: PluginConstructor = module.default;

    if (!Object.prototype.isPrototypeOf.call(Plugin, ctr)) {
      throw new GeneratorIOException(`Plugin '${modulePath}' is not an instance of a Plugin class`);
    }

    return ctr;
  }

  async read(filename: string): Promise<string> {
    const url = new URL(filename, this.#outputDir);
    this.#logger.global.debug(`Reading file: ${url}`);
    return await readFile(url, 'utf8');
  }

  async write(file: File): Promise<void> {
    const filePath = new URL(file.name, this.#outputDir);
    this.#logger.global.debug(`Writing file ${filePath}.`);
    const dir = new URL('./', filePath);
    await mkdir(dir, { recursive: true });
    return await writeFile(filePath, await file.text(), 'utf-8');
  }
}
